﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;

using MASAS.MSM.DomainLayer.Logging;
using MASAS.MSM.DomainLayer.Model;
using MASAS.MSM.Services.Data;
using MASAS.MSM.Services.Data.Geometries;

namespace MASAS.MSM.Services
{

    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "PublicationService" in both code and config file together.
    /// <summary>
    /// The Publication service is used to add a new feed entry to a MASAS Hub.
    /// </summary>
    public class PublicationService : IPublicationService
    {

        /// <summary>
        /// The maximum NBR of attachments accepted by the Hub.
        /// </summary>
        private const int cs_maxNbrOfAttachments = 5;

        /// <summary>
        /// Publishes a new entry.
        /// </summary>
        /// <param name="hub">The MASAS hub to receive the publication.</param>
        /// <param name="publicationEntry">The entry to be published.</param>
        /// <returns>
        /// The published entry with the additional server data.
        /// </returns>
        public FeedEntry PublishEntry( MASAS.MSM.Services.Data.Hub hub, PublicationEntry publicationEntry )
        {
            FeedEntry   feedEntry = new FeedEntry();
            string      pubMsg;
            string      contentType = "application/atom+xml";
            string      responseMsg;
            
            // Take the given attachments and create a list with only the ones we can actually publish...
            Attachment[] attachments = ValidateAttachments( publicationEntry );

            // Format publication to XML...
            pubMsg = GeneratePublicationXML( publicationEntry );

            // Add any attachments if needed...
            if( attachments.Length > 0 )
            {
                pubMsg = GeneratePublicationXMLWithAttachments( pubMsg, attachments );
                contentType = "multipart/related; boundary=0.a.unique.value.0";
            }

            // Send to Hub...
            responseMsg = PushDataToHub( hub.URI + "/feed", hub.Token, "POST", contentType, pubMsg, HttpStatusCode.Created );

            // Gather response and convert to FeedEntry...
            if( responseMsg != null )
            {
                feedEntry = ParseToFeedItem( responseMsg );
                Logger.AddLogEntry( "Entry id=" + feedEntry.Identifier + " has been created.", LogEventType.Information );
            }

            // Return data...
            return feedEntry;
        }

        /// <summary>
        /// Updates an entry.
        /// </summary>
        /// <param name="hub">The MASAS hub to receive the publication.</param>
        /// <param name="entryIdentifier">The entry identifier that needs to be updated.</param>
        /// <param name="publicationEntry">The publication entry with the data to be updated.</param>
        /// <returns>
        /// The published entry with the additional server data.
        /// </returns>
        public FeedEntry UpdateEntry( MASAS.MSM.Services.Data.Hub hub, string entryIdentifier, PublicationEntry publicationEntry )
        {
            FeedEntry   feedEntry = new FeedEntry();
            string      pubMsg;
            string      contentType = "application/atom+xml";
            string      responseMsg;
            
            // Take the given attachments and create a list with only the ones we can actually publish...
            Attachment[] attachments = ValidateAttachments( publicationEntry );

            // Format publication to XML...
            pubMsg = GeneratePublicationXML( publicationEntry );

            // Send the update to Hub...
            responseMsg = PushDataToHub( hub.URI + "/feed/" + entryIdentifier, hub.Token, "PUT", contentType, pubMsg, HttpStatusCode.OK );

            // Delete any existing attachments for now.
            // There is an issue where the published titles aren't being kept,
            // so it makes it that much harder to correlate between the items in 
            // our database and what's been published.
            responseMsg = PushDataToHub( hub.URI + "/feed/" + entryIdentifier + "/content", hub.Token, "DELETE", string.Empty, "", HttpStatusCode.OK );

            // Add any attachments if needed...
            if( attachments.Length == 1 )
            {
                Attachment attachment = attachments[0];

                if( attachment.ContentType == null || attachment.ContentType == string.Empty )
                {
                    string[] tmpValues = attachment.FileName.Split( '.' );
                    attachment.ContentType = MasasService.Instance.ContentTypeManager.GetContentType( tmpValues[tmpValues.Length - 1] );
                }

                pubMsg = attachment.Base64;
                contentType = attachment.ContentType;

                // Send the attachment update to Hub...
                responseMsg = PushDataToHub( hub.URI + "/feed/" + entryIdentifier + "/content/", hub.Token, "PUT", contentType, pubMsg, HttpStatusCode.OK, attachment.Title );
            }
            else if( attachments.Length > 1 )
            {
                pubMsg = GeneratePublicationXMLWithAttachments( string.Empty, attachments );
                contentType = "multipart/related; boundary=0.a.unique.value.0";

                // Send the attachment update to Hub...
                responseMsg = PushDataToHub( hub.URI + "/feed/" + entryIdentifier + "/content/", hub.Token, "PUT", contentType, pubMsg, HttpStatusCode.OK );
            }

            if( responseMsg != null )
            {
                // Gather response and convert to FeedEntry...
                feedEntry = ParseToFeedItem( responseMsg );
                Logger.AddLogEntry( "Entry id=" + entryIdentifier + " has been updated.", LogEventType.Information );
            }

            // Return data...
            return feedEntry;
        }

        /// <summary>
        /// Pushes publication data to the MASAS hub.
        /// </summary>
        /// <param name="uri">The Hub URI to push the data to.</param>
        /// <param name="token">The security token.</param>
        /// <param name="method">The method (POST/PUT).</param>
        /// <param name="pubMsg">The publication data.</param>
        /// <param name="statusCode">The status code expected on success.</param>
        /// <returns></returns>
        private string PushDataToHub( string uri, string token, string method, string contentType, string pubMsg, HttpStatusCode statusCode, string slug = null )
        {
            HttpWebRequest  request             = null;
            HttpWebResponse response            = null;
            Stream          dataStream          = null;
            string          responseFromServer  = null;
            byte[]          byteArray           = null;
            bool            hubError            = false;

            // Convert our message to a byte array...
            byteArray = Encoding.UTF8.GetBytes( pubMsg );

            // Create the HTTP request, and initialize as needed for publishing to HUB...
            request = ( HttpWebRequest )WebRequest.Create( uri );
            request.Method = method;
            
            if( contentType != null && contentType != string.Empty ) {
                request.ContentType = contentType;
            }

            request.Headers.Add( "Authorization", "MASAS-Secret " + token );

            if( slug != null && slug != string.Empty )
            {
                request.Headers.Add( "Content-Transfer-Encoding", "base64" );
                request.Headers.Add( "Slug", slug );
            }

            // Set the content length to match the buffer we are sending...
            request.ContentLength = byteArray.Length;

            // Get the request stream, and write our buffer to it...
            dataStream = request.GetRequestStream();
            dataStream.Write( byteArray, 0, byteArray.Length );
            dataStream.Close();

            try
            {
                // Get the response.
                response = ( HttpWebResponse )request.GetResponse();
            }
            catch( WebException webEx )
            {
                response = ( HttpWebResponse )webEx.Response;
                hubError = true;
                Logger.AddLogEntry( "Publish to Hub communication error: " + response.StatusDescription + " (Code=" + ( ( int )response.StatusCode ).ToString() + ").", webEx );
            }
            catch( Exception ex )
            {
                hubError = true;
                Logger.AddLogEntry( "Publish to Hub communication error: An exception occured!", ex );
            }

            // No communication error...
            if( hubError == false )
            {
                // Do we have the expected status code?
                if( response.StatusCode == statusCode )
                {
                    // Get the server's response stream...
                    dataStream = response.GetResponseStream();

                    // Read the content...
                    StreamReader reader = new StreamReader( dataStream );
                    responseFromServer = reader.ReadToEnd();

                    // Clean up the resources...
                    reader.Close();
                    dataStream.Close();
                }
                else
                {
                    Logger.AddLogEntry( "Hub communication error: An unexpected status code was returned (Code=" + response.StatusCode.ToString() + ")." );
                }
            }

            // Clean up...
            response.Close();

            return responseFromServer;
        }

        private Attachment[] ValidateAttachments( PublicationEntry publicationEntry )
        {
            List<Attachment> attachments = new List<Attachment>();

            foreach( Attachment attachment in publicationEntry.Attachments )
            {
                if( attachment.ContentType == null || attachment.ContentType == string.Empty )
                {
                    string[] tmpValues = attachment.FileName.Split( '.' );
                    attachment.ContentType = MasasService.Instance.ContentTypeManager.GetContentType( tmpValues[tmpValues.Length - 1] );
                }

                if( MasasService.Instance.ContentTypeManager.IsValid( attachment.ContentType ) )
                {
                    attachments.Add( attachment );
                }
            }

            int nbrOfAttachments = attachments.Count;
            if( attachments.Count > cs_maxNbrOfAttachments ) {
                nbrOfAttachments = cs_maxNbrOfAttachments;
                Logger.AddLogEntry( "Maximum number of attachments has been reached.", LogEventType.Information );
            }

            // Return at most the MAX NUMBER OF ATTACHMENTS!
            return ( attachments.GetRange( 0, nbrOfAttachments ) ).ToArray();
        }

        private string GeneratePublicationXMLWithAttachments( string entryData, Attachment[] attachments )
        {
            string newData = "";

            if( entryData != null && entryData != string.Empty )
            {
                newData = "--0.a.unique.value.0\r\n";
                newData += "Content-Disposition: attachment; name=\"entry\"; filename=\"entry.xml\"\r\n";
                newData += "Content-Type: application/atom+xml\r\n";
                newData += "\r\n";
                newData += entryData;
                newData += "\r\n";
            }

            for( int iCtr = 0; iCtr < attachments.Length; iCtr++ )
            {
                Attachment attachment = attachments[iCtr];

                newData += "--0.a.unique.value.0\r\n";
                newData += "Content-Disposition: attachment; name=\"" + attachment.Title + "\"; filename=\"" + attachment.FileName + "\"\r\n";

                newData += "Content-Type: " + attachment.ContentType + "\r\n";
                newData += "Content-Transfer-Encoding: base64\r\n";
                newData += "\r\n";
                newData += attachment.Base64;
                newData += "\r\n";
            }

            newData += "--0.a.unique.value.0--\r\n";

            return newData;
        }

        /// <summary>
        /// Generates the publication XML from the entry.
        /// </summary>
        /// <param name="publicationEntry">The publication entry.</param>
        /// <returns>The XML representation of that entry.</returns>
        private string GeneratePublicationXML( PublicationEntry publicationEntry )
        {
            StringBuilder feedXML = new StringBuilder();

            feedXML.AppendLine( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" );
            feedXML.AppendLine( "<entry xmlns=\"http://www.w3.org/2005/Atom\" xmlns:georss=\"http://www.georss.org/georss\" xmlns:app=\"http://www.w3.org/2007/app\" xmlns:age=\"http://purl.org/atompub/age/1.0\">" );
            
            // Status...
            feedXML.AppendFormat( "<category label=\"Status\" scheme=\"masas:category:status\" term=\"{0}\" />", publicationEntry.Status.ToString() );
            feedXML.AppendLine();

            // Severity...
            if( publicationEntry.Severities != null && publicationEntry.Severities.Count > 0 )
            {
                foreach( SeverityTypes severity in publicationEntry.Severities )
                {
                    feedXML.AppendFormat( "<category label=\"Severity\" scheme=\"masas:category:severity\" term=\"{0}\" />", severity.ToString() );
                    feedXML.AppendLine();
                }
            }

            // Icon...
            if( publicationEntry.Icon != null && publicationEntry.Icon.Length > 0 )
            {
                feedXML.AppendFormat( "<category label=\"Icon\" scheme=\"masas:category:icon\" term=\"{0}\" />", publicationEntry.Icon );
                feedXML.AppendLine();
            }

            // Category...
            if( publicationEntry.Categories != null && publicationEntry.Categories.Count > 0 )
            {
                foreach( CategoryTypes category in publicationEntry.Categories )
                {
                    feedXML.AppendFormat( "<category label=\"Category\" scheme=\"masas:category:category\" term=\"{0}\" />", category.ToString() );
                    feedXML.AppendLine();
                }
            }

            // Certainty...
            if( publicationEntry.Certainties != null && publicationEntry.Certainties.Count > 0 )
            {
                foreach( CertaintyTypes certainty in publicationEntry.Certainties )
                {
                    feedXML.AppendFormat( "<category label=\"Certainty\" scheme=\"masas:category:certainty\" term=\"{0}\" />", certainty.ToString() );
                    feedXML.AppendLine();
                }
            }

            // Content...
            feedXML.AppendLine( "<content type=\"xhtml\">" );
            feedXML.AppendLine( "<div xmlns=\"http://www.w3.org/1999/xhtml\">" );
            feedXML.AppendFormat( "<div xml:lang=\"en\">{0}</div>", publicationEntry.Content );
            feedXML.AppendLine();
            feedXML.AppendLine( "</div>" );
            feedXML.AppendLine( "</content>" );

            // Title...
            feedXML.AppendLine("<title type=\"xhtml\">");
            feedXML.AppendLine("<div xmlns=\"http://www.w3.org/1999/xhtml\">");
            feedXML.AppendFormat("<div xml:lang=\"en\">{0}</div>", publicationEntry.Title );
            feedXML.AppendLine();
            feedXML.AppendLine("</div>");
            feedXML.AppendLine("</title>");

            // Summary...
            if( publicationEntry.Summary != null && publicationEntry.Summary.Length > 0 )
            {
                feedXML.AppendLine( "<summary type=\"xhtml\">" );
                feedXML.AppendLine( "<div xmlns=\"http://www.w3.org/1999/xhtml\">" );
                feedXML.AppendFormat( "<div xml:lang=\"en\">{0}</div>", publicationEntry.Summary );
                feedXML.AppendLine();
                feedXML.AppendLine( "</div>" );
                feedXML.AppendLine( "</summary>" );
            }

            // Rights...
            // TODO!
            
            // Geometries (GeoRSS Simple)...
            if( publicationEntry.Geometries != null && publicationEntry.Geometries.Count > 0 )
            {
                foreach( Geometry geometry in publicationEntry.Geometries )
                {
                    feedXML.AppendFormat( "{0}", geometry.ToGeoRSS() );
                    feedXML.AppendLine();
                }
            }

            // Expiration (RFC 3339 in UTC)...
            feedXML.AppendFormat( "<age:expires>{0}Z</age:expires>", publicationEntry.Expiration.ToUniversalTime().ToString( "s" ) );
            feedXML.AppendLine();

            // Related Links...
            if( publicationEntry.RelatedLinks != null && publicationEntry.RelatedLinks.Count > 0 )
            {
                foreach( string link in publicationEntry.RelatedLinks )
                {
                    feedXML.AppendFormat( "<link href=\"{0}\" rel=\"related\" />", link );
                    feedXML.AppendLine();
                }
            }

            // Close the entry tag...
            feedXML.AppendLine( "</entry>" );

            return feedXML.ToString();
        }

        /// <summary>
        /// Parses the given XML to a feed item.
        /// </summary>
        /// <param name="responseMsg">The XML response message.</param>
        /// <returns>The FeedEntry populated with the XML.</returns>
        private FeedEntry ParseToFeedItem( string responseMsg )
        {
            // NOTE: this process could be improved at a later time.
            FeedEntry feedEntry = new FeedEntry();

            // Create a syndication item from the XML...
            StringReader strReader = new StringReader( responseMsg );
            XmlReader reader = XmlReader.Create( strReader );
            SyndicationItem feedItem = SyndicationItem.Load( reader );

            // Convert the syndication item to an internal Hub entry...
            HubEntry hubEntry = HubEntry.Create( feedItem );

            // Copy the Hub entry to a simple feed entry...
            feedEntry.Identifier    = hubEntry.ExternalIdentifier;
            feedEntry.Author        = hubEntry.Author;
            feedEntry.Title         = hubEntry.Title;
            feedEntry.Content       = hubEntry.Content;
            feedEntry.Summary       = hubEntry.Summary;
            feedEntry.Published     = hubEntry.Published;
            feedEntry.LastUpdated   = hubEntry.LastUpdated;
            feedEntry.Expiration    = hubEntry.Expiration;
            feedEntry.Categories    = hubEntry.Categories;
            feedEntry.Status        = hubEntry.Status;
            feedEntry.Severities    = hubEntry.Severities;
            feedEntry.Certainties   = hubEntry.Certainties;
            feedEntry.Icon          = hubEntry.Symbol;

            List<Attachment> attachments = new List<Attachment>();
            foreach( EntryAttachment entryAttachment in hubEntry.Attachments )
            {
                Attachment attachment = new Attachment();
                attachment.ContentType = entryAttachment.ContentType;
                attachment.FileName = entryAttachment.HRef.AbsoluteUri;
                attachment.Title = entryAttachment.Title;
                attachments.Add( attachment );
            }
            feedEntry.Attachments = attachments.ToArray();

            // TODO:
            //feedEntry.Geometries = hubEntry.Geometries;
            //feedEntry.RelatedLinks = hubEntry.RelatedLinks;

            return feedEntry;
        }

    }

}
